// Cascade Studio Standard Library
// Adding new standard library features and functions:
// 1. Research the OpenCascade API: https://www.opencascade.com/doc/occt-7.4.0/refman/html/annotated.html
// 2. Write your new function inside of Cascade Studio, using "oc." to refer to the raw OpenCascade API.
// 3. Add your new convenience function to this file
// 4. Add typescript annotations to index.ts in this same directory
// 5. Submit a PR to the main repository! https://github.com/zalo/CascadeStudio/pulls
// -
// (Optional) If base functions appear to be missing, fork opencascade.js and add them to this file:
//  - https://github.com/donalffons/opencascade.js/blob/master/opencascade.idl
//  - Upon push, Github Actions will build a new version of the library and commit it back to the repo
//  - From there, you can graft those into CascadeStudio/node_modules/opencascade.js/dist (following its existing conventions)

/* eslint-disable */
/* global sceneShapes, argCache */

let argCache = {};
let sceneShapes = [];

function Box (x, y, z, centered) {
	if (!centered) { centered = false; }
	const curBox = CacheOp(arguments, () => {
		// Construct a Box Primitive
		const box = new oc.BRepPrimAPI_MakeBox(x, y, z).Shape();
		if (centered) {
			return Translate([-x / 2, -y / 2, -z / 2], box);
		} else {
			return box;
		}
	});

	sceneShapes.push(curBox);
	return curBox;
}

function Sphere (radius) {
	const curSphere = CacheOp(arguments, () => {
		// Construct a Sphere Primitive
		const spherePlane = new oc.gp_Ax2(new oc.gp_Pnt(0, 0, 0), oc.gp.prototype.DZ());
		return new oc.BRepPrimAPI_MakeSphere(spherePlane, radius).Shape();
	});

	sceneShapes.push(curSphere);
	return curSphere;
}

function Cylinder (radius, height, centered) {
	const curCylinder = CacheOp(arguments, () => {
		const cylinderPlane = new oc.gp_Ax2(new oc.gp_Pnt(0, 0, centered ? -height / 2 : 0), new oc.gp_Dir(0, 0, 1));
		return new oc.BRepPrimAPI_MakeCylinder(cylinderPlane, radius, height).Shape();
	});
	sceneShapes.push(curCylinder);
	return curCylinder;
}

function Cone (radius1, radius2, height) {
	const curCone = CacheOp(arguments, () => {
		return new oc.BRepPrimAPI_MakeCone(radius1, radius2, height).Shape();
	});
	sceneShapes.push(curCone);
	return curCone;
}

function Polygon (points, wire) {
	const curPolygon = CacheOp(arguments, () => {
		const gpPoints = [];
		for (let ind = 0; ind < points.length; ind++) {
			gpPoints.push(convertToPnt(points[ind]));
		}

		const polygonWire = new oc.BRepBuilderAPI_MakeWire();
		for (let ind = 0; ind < points.length - 1; ind++) {
			const seg = new oc.GC_MakeSegment(gpPoints[ind], gpPoints[ind + 1]).Value();
			const edge = new oc.BRepBuilderAPI_MakeEdge(seg).Edge();
			const innerWire = new oc.BRepBuilderAPI_MakeWire(edge).Wire();
			polygonWire.Add(innerWire);
		}
		const seg2 = new oc.GC_MakeSegment(gpPoints[points.length - 1], gpPoints[0]).Value();
		const edge2 = new oc.BRepBuilderAPI_MakeEdge(seg2).Edge();
		const innerWire2 = new oc.BRepBuilderAPI_MakeWire(edge2).Wire();
		polygonWire.Add(innerWire2);
		const finalWire = polygonWire.Wire();

		if (wire) {
			return finalWire;
		} else {
			return new oc.BRepBuilderAPI_MakeFace(finalWire).Face();
		}
	});
	sceneShapes.push(curPolygon);
	return curPolygon;
}

function Circle (radius, wire) {
	const curCircle = CacheOp(arguments, () => {
		const circle = new oc.GC_MakeCircle(new oc.gp_Ax2(new oc.gp_Pnt(0, 0, 0),
			new oc.gp_Dir(0, 0, 1)), radius).Value();
		const edge = new oc.BRepBuilderAPI_MakeEdge(circle).Edge();
		const circleWire = new oc.BRepBuilderAPI_MakeWire(edge).Wire();
		if (wire) { return circleWire; }
		return new oc.BRepBuilderAPI_MakeFace(circleWire).Face();
	});
	sceneShapes.push(curCircle);
	return curCircle;
}

function BSpline (inPoints, closed) {
	const curSpline = CacheOp(arguments, () => {
		const ptList = new oc.TColgp_Array1OfPnt(1, inPoints.length + (closed ? 1 : 0));
		for (let pIndex = 1; pIndex <= inPoints.length; pIndex++) {
			ptList.SetValue(pIndex, convertToPnt(inPoints[pIndex - 1]));
		}
		if (closed) { ptList.SetValue(inPoints.length + 1, ptList.Value(1)); }

		const geomCurveHandle = new oc.GeomAPI_PointsToBSpline(ptList).Curve();
		const edge = new oc.BRepBuilderAPI_MakeEdge(geomCurveHandle).Edge();
		return new oc.BRepBuilderAPI_MakeWire(edge).Wire();
	});
	sceneShapes.push(curSpline);
	return curSpline;
}

function Text3D (text, size, height, fontName) {
	if (!size) { size = 36; }
	if (!height && height !== 0.0) { height = 0.15; }
	if (!fontName) { fontName = 'Roboto'; }

	const textArgs = JSON.stringify(arguments);
	const curText = CacheOp(arguments, () => {
		if (fonts[fontName] === undefined) {
			argCache = {};
			console.log('Font not loaded or found yet!  Try again...');
			return;
		}
		const textFaces = [];
		const commands = fonts[fontName].getPath(text, 0, 0, size).commands;
		for (let idx = 0; idx < commands.length; idx++) {
			if (commands[idx].type === 'M') {
				// Start a new Glyph
				var firstPoint = new oc.gp_Pnt(commands[idx].x, commands[idx].y, 0);
				var lastPoint = firstPoint;
				var currentWire = new oc.BRepBuilderAPI_MakeWire();
			} else if (commands[idx].type === 'Z') {
				// End the current Glyph and Finish the Path
				try {
					let faceBuilder = null;
					if (textFaces.length > 0) {
						faceBuilder = new oc.BRepBuilderAPI_MakeFace(
							textFaces[textFaces.length - 1], currentWire.Wire());
					} else {
						faceBuilder = new oc.BRepBuilderAPI_MakeFace(currentWire.Wire());
					}

					textFaces.push(faceBuilder.Face());
				} catch (e) {
					console.error('ERROR: OCC encountered malformed characters when constructing faces from this font (likely self-intersections)!  Try using a more robust font like \'Roboto\'.');
				}
			} else if (commands[idx].type === 'L') {
				const nextPoint = new oc.gp_Pnt(commands[idx].x, commands[idx].y, 0);
				if (lastPoint.X() === nextPoint.X() && lastPoint.Y() === nextPoint.Y()) { continue; }
				const lineSegment = new oc.GC_MakeSegment(lastPoint, nextPoint).Value();
				const lineEdge = new oc.BRepBuilderAPI_MakeEdge(lineSegment).Edge();
				currentWire.Add(new oc.BRepBuilderAPI_MakeWire(lineEdge).Wire());
				lastPoint = nextPoint;
			} else if (commands[idx].type === 'Q') {
				const controlPoint = new oc.gp_Pnt(commands[idx].x1, commands[idx].y1, 0);
				const nextPoint = new oc.gp_Pnt(commands[idx].x, commands[idx].y, 0);

				const ptList = new oc.TColgp_Array1OfPnt(1, 3);
				ptList.SetValue(1, lastPoint);
				ptList.SetValue(2, controlPoint);
				ptList.SetValue(3, nextPoint);
				const quadraticCurve = new oc.Geom_BezierCurve(ptList);
				const lineEdge = new oc.BRepBuilderAPI_MakeEdge(new oc.Handle_Geom_BezierCurve(quadraticCurve)).Edge();
				currentWire.Add(new oc.BRepBuilderAPI_MakeWire(lineEdge).Wire());

				lastPoint = nextPoint;
			} else if (commands[idx].type === 'C') {
				const controlPoint1 = new oc.gp_Pnt(commands[idx].x1, commands[idx].y1, 0);
				const controlPoint2 = new oc.gp_Pnt(commands[idx].x2, commands[idx].y2, 0);
				const nextPoint = new oc.gp_Pnt(commands[idx].x, commands[idx].y, 0);

				const ptList = new oc.TColgp_Array1OfPnt(1, 4);
				ptList.SetValue(1, lastPoint);
				ptList.SetValue(2, controlPoint1);
				ptList.SetValue(3, controlPoint2);
				ptList.SetValue(4, nextPoint);
				const cubicCurve = new oc.Geom_BezierCurve(ptList);
				const lineEdge = new oc.BRepBuilderAPI_MakeEdge(new oc.Handle_Geom_BezierCurve(cubicCurve)).Edge();
				currentWire.Add(new oc.BRepBuilderAPI_MakeWire(lineEdge).Wire());

				lastPoint = nextPoint;
			}
		}

		if (height === 0) {
			return textFaces[textFaces.length - 1];
		} else {
			textFaces[textFaces.length - 1].hash = stringToHash(textArgs);
			const textSolid = Rotate([1, 0, 0], -90, Extrude(textFaces[textFaces.length - 1], [0, 0, height * size]));
			sceneShapes = Remove(sceneShapes, textSolid);
			return textSolid;
		}
	});

	sceneShapes.push(curText);
	return curText;
}

// These foreach functions are not cache friendly right now!
function ForEachSolid (shape, callback) {
	let solid_index = 0;
	const anExplorer = new oc.TopExp_Explorer(shape, oc.TopAbs_SOLID);
	for (anExplorer.Init(shape, oc.TopAbs_SOLID); anExplorer.More(); anExplorer.Next()) {
		callback(solid_index++, oc.TopoDS.prototype.Solid(anExplorer.Current()));
	}
}

function GetNumSolidsInCompound (shape) {
	if (!shape || shape.ShapeType() > 1 || shape.IsNull()) {
		console.error('Not a compound shape!');
		return shape;
	}
	let solidsFound = 0;
	ForEachSolid(shape, (i, s) => { solidsFound++; });
	return solidsFound;
}

function GetSolidFromCompound (shape, index, keepOriginal) {
	if (!shape || shape.ShapeType() > 1 || shape.IsNull()) {
		console.error('Not a compound shape!');
		return shape;
	}
	if (!index) { index = 0; }

	const sol = CacheOp(arguments, () => {
		let innerSolid = {};
		let solidsFound = 0;
		ForEachSolid(shape, (i, s) => {
			if (i === index) { innerSolid = new oc.TopoDS_Solid(s); }
			solidsFound++;
		});
		if (solidsFound === 0) {
			console.error('NO SOLIDS FOUND IN SHAPE!');
			innerSolid = shape;
		}
		innerSolid.hash = shape.hash + 1;
		return innerSolid;
	});

	if (!keepOriginal) { sceneShapes = Remove(sceneShapes, shape); }
	sceneShapes.push(sol);

	return sol;
}

export function ForEachShell (shape, callback) {
	let shell_index = 0;
	const anExplorer = new oc.TopExp_Explorer(shape, oc.TopAbs_SHELL);
	for (anExplorer.Init(shape, oc.TopAbs_SHELL); anExplorer.More(); anExplorer.Next()) {
		callback(shell_index++, oc.TopoDS.prototype.Shell(anExplorer.Current()));
	}
}

export function ForEachFace (shape, callback) {
	let face_index = 0;
	const faceHashes = [];
	const anExplorer = new oc.TopExp_Explorer(shape, oc.TopAbs_FACE);
	for (anExplorer.Init(shape, oc.TopAbs_FACE); anExplorer.More(); anExplorer.Next()) {
		// callback(face_index++, oc.TopoDS.prototype.Face(anExplorer.Current()));

		// --------------------
		const face = oc.TopoDS.prototype.Face(anExplorer.Current());
		const faceHash = face.HashCode(100000000);
		if (!faceHashes.hasOwnProperty(faceHash)) {
			faceHashes[faceHash] = face_index;
			callback(face_index++, face);
		}
		// --------------------
	}
}

function ForEachWire (shape, callback) {
	let wire_index = 0;
	const anExplorer = new oc.TopExp_Explorer(shape, oc.TopAbs_WIRE);
	for (anExplorer.Init(shape, oc.TopAbs_WIRE); anExplorer.More(); anExplorer.Next()) {
		callback(wire_index++, oc.TopoDS.prototype.Wire(anExplorer.Current()));
	}
}

function GetWire (shape, index, keepOriginal) {
	if (!shape || shape.ShapeType() > 4 || shape.IsNull()) {
		console.error('Not a wire shape!');
		return shape;
	}
	if (!index) { index = 0; }

	const wire = CacheOp(arguments, () => {
		let innerWire = {};
		let wiresFound = 0;
		ForEachWire(shape, (i, s) => {
			if (i === index) { innerWire = new oc.TopoDS_Wire(s); }
			wiresFound++;
		});
		if (wiresFound === 0) {
			console.error('NO WIRES FOUND IN SHAPE!');
			innerWire = shape;
		}
		innerWire.hash = shape.hash + 1;
		return innerWire;
	});

	if (!keepOriginal) { sceneShapes = Remove(sceneShapes, shape); }
	sceneShapes.push(wire);

	return wire;
}

export function ForEachEdge (shape, callback) {
	const edgeHashes = {};
	let edgeIndex = 0;
	const anExplorer = new oc.TopExp_Explorer(shape, oc.TopAbs_EDGE);
	for (anExplorer.Init(shape, oc.TopAbs_EDGE); anExplorer.More(); anExplorer.Next()) {
		const edge = oc.TopoDS.prototype.Edge(anExplorer.Current());
		const edgeHash = edge.HashCode(100000000);
		if (!edgeHashes.hasOwnProperty(edgeHash)) {
			edgeHashes[edgeHash] = edgeIndex;
			callback(edgeIndex++, edge);
		}
	}
	return edgeHashes;
}

export function ForEachVertex (shape, callback) {
	const anExplorer = new oc.TopExp_Explorer(shape, oc.TopAbs_VERTEX);
	for (anExplorer.Init(shape, oc.TopAbs_VERTEX); anExplorer.More(); anExplorer.Next()) {
		callback(oc.TopoDS.prototype.Vertex(anExplorer.Current()));
	}
}

export function FilletEdges (shape, radius, edgeList, keepOriginal) {
	const curFillet = CacheOp(arguments, () => {
		const mkFillet = new oc.BRepFilletAPI_MakeFillet(shape);
		let foundEdges = 0;
		ForEachEdge(shape, (index, edge) => {
			if (edgeList.includes(index)) {
				mkFillet.Add(radius, edge);
				foundEdges++;
			}
		});
		if (foundEdges === 0) {
			console.error('Fillet Edges Not Found!  Make sure you are looking at the object _before_ the Fillet is applied!');
			return new oc.TopoDS_Solid(shape);
		}
		return new oc.TopoDS_Solid(mkFillet.Shape());
	});
	sceneShapes.push(curFillet);
	if (!keepOriginal) { sceneShapes = Remove(sceneShapes, shape); }
	return curFillet;
}

export function ChamferEdges (shape, distance, edgeList, keepOriginal) {
	const curChamfer = CacheOp(arguments, () => {
		const mkChamfer = new oc.BRepFilletAPI_MakeChamfer(shape);
		let foundEdges = 0;
		ForEachEdge(shape, (index, edge) => {
			if (edgeList.includes(index)) {
				mkChamfer.Add(distance, edge);
				foundEdges++;
			}
		});
		if (foundEdges === 0) {
			console.error('Chamfer Edges Not Found!  Make sure you are looking at the object _before_ the Chamfer is applied!');
			return new oc.TopoDS_Solid(shape);
		}
		return new oc.TopoDS_Solid(mkChamfer.Shape());
	});
	sceneShapes.push(curChamfer);
	if (!keepOriginal) { sceneShapes = Remove(sceneShapes, shape); }
	return curChamfer;
}

function Transform (translation, rotation, scale, shapes) {
	const args = arguments;
	return CacheOp(arguments, () => {
		if (args.length === 4) {
			// Create the transform gizmo and add it to the scene
			postMessage({ 'type': 'createTransformHandle', 'payload': { translation: translation, rotation: rotation, scale: scale, lineAndColumn: getCallingLocation() } });
			// Transform the Object(s)
			return Translate(translation, Rotate(rotation[0], rotation[1], Scale(scale, shapes)));
		} else {
			// Create the transform gizmo and add it to the scene
			postMessage({ 'type': 'createTransformHandle', 'payload': { translation: [0, 0, 0], rotation: [[0, 1, 0], 1], scale: 1, lineAndColumn: getCallingLocation() } });
			return translation; // The first element will be the shapes
		}
	});
}

export function Translate (offset, shapes, keepOriginal) {
	const translated = CacheOp(arguments, () => {
		const transformation = new oc.gp_Trsf();
		transformation.SetTranslation(new oc.gp_Vec(offset[0], offset[1], offset[2]));
		const translation = new oc.TopLoc_Location(transformation);
		if (!isArrayLike(shapes)) {
			return new oc.TopoDS_Shape(shapes.Moved(translation));
		} else if (shapes.length >= 1) {      // Do the normal translation
			const newTrans = [];
			for (let shapeIndex = 0; shapeIndex < shapes.length; shapeIndex++) {
				newTrans.push(new oc.TopoDS_Shape(shapes[shapeIndex].Moved(translation)));
			}
			return newTrans;
		}
	});

	if (!keepOriginal) { sceneShapes = Remove(sceneShapes, shapes); }
	sceneShapes.push(translated);

	return translated;
}

function Rotate (axis, degrees, shapes, keepOriginal) {
	let rotated = null;
	if (degrees === 0) {
		rotated = new oc.TopoDS_Shape(shapes);
	} else {
		rotated = CacheOp(arguments, () => {
			let newRot;
			const transformation = new oc.gp_Trsf();
			transformation.SetRotation(
				new oc.gp_Ax1(new oc.gp_Pnt(0, 0, 0), new oc.gp_Dir(
					new oc.gp_Vec(axis[0], axis[1], axis[2]))), degrees * 0.0174533);
			const rotation = new oc.TopLoc_Location(transformation);
			if (!isArrayLike(shapes)) {
				newRot = new oc.TopoDS_Shape(shapes.Moved(rotation));
			} else if (shapes.length >= 1) {      // Do the normal rotation
				for (let shapeIndex = 0; shapeIndex < shapes.length; shapeIndex++) {
					shapes[shapeIndex].Move(rotation);
				}
			}
			return newRot;
		});
	}
	if (!keepOriginal) { sceneShapes = Remove(sceneShapes, shapes); }
	sceneShapes.push(rotated);
	return rotated;
}

function Mirror (vector, shapes, keepOriginal) {
	if (!vector) { vector = [1, 0, 0]; }
	const mirrored = CacheOp(arguments, () => {
		const mirrorTransform = new oc.gp_Trsf();
		const mirrorPlaneOrigin = new oc.gp_Pnt(0, 0, 0);
		const mirrorPlaneNormal = new oc.gp_Dir(vector[0], vector[1], vector[2]);
		mirrorTransform.SetMirror(new oc.gp_Ax2(mirrorPlaneOrigin, mirrorPlaneNormal));

		if (!isArrayLike(shapes)) {
			return new oc.BRepBuilderAPI_Transform(shapes, mirrorTransform).Shape();
		} else if (shapes.length >= 1) {
			const newMirroring = [];
			for (let shapeIndex = 0; shapeIndex < shapes.length; shapeIndex++) {
				newMirroring.push(new oc.BRepBuilderAPI_Transform(shapes, mirrorTransform).Shape());
			}
			return newMirroring;
		}
	});
	if (!keepOriginal) { sceneShapes = Remove(sceneShapes, shapes); }
	sceneShapes.push(mirrored);

	return mirrored;
}

function Scale (scale, shapes, keepOriginal) {
	const scaled = CacheOp(arguments, () => {
		const transformation = new oc.gp_Trsf();
		transformation.SetScaleFactor(scale);
		const scaling = new oc.TopLoc_Location(transformation);
		if (!isArrayLike(shapes)) {
			return new oc.TopoDS_Shape(shapes.Moved(scaling));
		} else if (shapes.length >= 1) {      // Do the normal rotation
			const newScale = [];
			for (let shapeIndex = 0; shapeIndex < shapes.length; shapeIndex++) {
				newScale.push(new oc.TopoDS_Shape(shapes[shapeIndex].Moved(scaling)));
			}
			return newScale;
		}
	});

	if (!keepOriginal) { sceneShapes = Remove(sceneShapes, shapes); }
	sceneShapes.push(scaled);

	return scaled;
}

// TODO: These ops can be more cache optimized since they're multiple sequential ops
function Union (objectsToJoin, keepObjects, fuzzValue, keepEdges) {
	if (!fuzzValue) { fuzzValue = 0.1; }
	const curUnion = CacheOp(arguments, () => {
		let combined = new oc.TopoDS_Shape(objectsToJoin[0]);
		if (objectsToJoin.length > 1) {
			for (let i = 0; i < objectsToJoin.length; i++) {
				if (i > 0) {
					const combinedFuse = new oc.BRepAlgoAPI_Fuse(combined, objectsToJoin[i]);
					combinedFuse.SetFuzzyValue(fuzzValue);
					combinedFuse.Build();
					combined = combinedFuse.Shape();
				}
			}
		}

		if (!keepEdges) {
			const fusor = new oc.ShapeUpgrade_UnifySameDomain(combined);
			fusor.Build();
			combined = fusor.Shape();
		}

		return combined;
	});

	for (let i = 0; i < objectsToJoin.length; i++) {
		if (!keepObjects) { sceneShapes = Remove(sceneShapes, objectsToJoin[i]); }
	}
	sceneShapes.push(curUnion);
	return curUnion;
}

function Difference (mainBody, objectsToSubtract, keepObjects, fuzzValue, keepEdges) {
	if (!fuzzValue) { fuzzValue = 0.1; }
	const curDifference = CacheOp(arguments, () => {
		if (!mainBody || mainBody.IsNull()) { console.error('Main Shape in Difference is null!'); }

		let difference = new oc.TopoDS_Shape(mainBody);
		if (objectsToSubtract.length >= 1) {
			for (let i = 0; i < objectsToSubtract.length; i++) {
				if (!objectsToSubtract[i] || objectsToSubtract[i].IsNull()) { console.error('Tool in Difference is null!'); }
				const differenceCut = new oc.BRepAlgoAPI_Cut(difference, objectsToSubtract[i]);
				differenceCut.SetFuzzyValue(fuzzValue);
				differenceCut.Build();
				difference = differenceCut.Shape();
			}
		}

		if (!keepEdges) {
			const fusor = new oc.ShapeUpgrade_UnifySameDomain(difference);
			fusor.Build();
			difference = fusor.Shape();
		}

		difference.hash = ComputeHash(arguments);
		if (GetNumSolidsInCompound(difference) === 1) {
			difference = GetSolidFromCompound(difference, 0);
		}

		return difference;
	});

	if (Array.isArray(keepObjects)) {
		if (!keepObjects[0]) { sceneShapes = Remove(sceneShapes, mainBody); }
		for (let i = 0; i < objectsToSubtract.length; i++) {
			if (!keepObjects[1]) { sceneShapes = Remove(sceneShapes, objectsToSubtract[i]); }
		}
	} else {
		if (!keepObjects) { sceneShapes = Remove(sceneShapes, mainBody); }
		for (let i = 0; i < objectsToSubtract.length; i++) {
			if (!keepObjects) { sceneShapes = Remove(sceneShapes, objectsToSubtract[i]); }
		}
	}

	sceneShapes.push(curDifference);
	return curDifference;
}

function Intersection (objectsToIntersect, keepObjects, fuzzValue, keepEdges) {
	if (!fuzzValue) { fuzzValue = 0.1; }
	const curIntersection = CacheOp(arguments, () => {
		let intersected = new oc.TopoDS_Shape(objectsToIntersect[0]);
		if (objectsToIntersect.length > 1) {
			for (let i = 0; i < objectsToIntersect.length; i++) {
				if (i > 0) {
					const intersectedCommon = new oc.BRepAlgoAPI_Common(intersected, objectsToIntersect[i]);
					intersectedCommon.SetFuzzyValue(fuzzValue);
					intersectedCommon.Build();
					intersected = intersectedCommon.Shape();
				}
			}
		}

		if (!keepEdges) {
			const fusor = new oc.ShapeUpgrade_UnifySameDomain(intersected);
			fusor.Build();
			intersected = fusor.Shape();
		}

		return intersected;
	});

	for (let i = 0; i < objectsToIntersect.length; i++) {
		if (!keepObjects) { sceneShapes = Remove(sceneShapes, objectsToIntersect[i]); }
	}
	sceneShapes.push(curIntersection);
	return curIntersection;
}

function Extrude (face, direction, keepFace) {
	const curExtrusion = CacheOp(arguments, () => {
		return new oc.BRepPrimAPI_MakePrism(face,
			new oc.gp_Vec(direction[0], direction[1], direction[2])).Shape();
	});

	if (!keepFace) { sceneShapes = Remove(sceneShapes, face); }
	sceneShapes.push(curExtrusion);
	return curExtrusion;
}

function RemoveInternalEdges (shape, keepShape) {
	const cleanShape = CacheOp(arguments, () => {
		const fusor = new oc.ShapeUpgrade_UnifySameDomain(shape);
		fusor.Build();
		return fusor.Shape();
	});

	if (!keepShape) { sceneShapes = Remove(sceneShapes, shape); }
	sceneShapes.push(cleanShape);
	return cleanShape;
}

function Offset (shape, offsetDistance, tolerance, keepShape) {
	if (!shape || shape.IsNull()) { console.error('Offset received Null Shape!'); }
	if (!tolerance) { tolerance = 0.1; }
	if (offsetDistance === 0.0) { return shape; }
	const curOffset = CacheOp(arguments, () => {
		let offset = null;
		if (shape.ShapeType() === 5) {
			offset = new oc.BRepOffsetAPI_MakeOffset();
			offset.AddWire(shape);
			offset.Perform(offsetDistance);
		} else {
			offset = new oc.BRepOffsetAPI_MakeOffsetShape();
			offset.PerformByJoin(shape, offsetDistance, tolerance);
		}
		let offsetShape = new oc.TopoDS_Shape(offset.Shape());

		// Convert Shell to Solid as is expected
		if (offsetShape.ShapeType() === 3) {
			const solidOffset = new oc.BRepBuilderAPI_MakeSolid();
			solidOffset.Add(offsetShape);
			offsetShape = new oc.TopoDS_Solid(solidOffset.Solid());
		}

		return offsetShape;
	});

	if (!keepShape) { sceneShapes = Remove(sceneShapes, shape); }
	sceneShapes.push(curOffset);
	return curOffset;
}

function Revolve (shape, degrees, direction, keepShape, copy) {
	if (!degrees) { degrees = 360.0; }
	if (!direction) { direction = [0, 0, 1]; }
	const curRevolution = CacheOp(arguments, () => {
		if (degrees >= 360.0) {
			return new oc.BRepPrimAPI_MakeRevol(shape,
				new oc.gp_Ax1(new oc.gp_Pnt(0, 0, 0),
					new oc.gp_Dir(direction[0], direction[1], direction[2])),
				copy).Shape();
		} else {
			return new oc.BRepPrimAPI_MakeRevol(shape,
				new oc.gp_Ax1(new oc.gp_Pnt(0, 0, 0),
					new oc.gp_Dir(direction[0], direction[1], direction[2])),
				degrees * 0.0174533, copy).Shape();
		}
	});

	if (!keepShape) { sceneShapes = Remove(sceneShapes, shape); }
	sceneShapes.push(curRevolution);
	return curRevolution;
}

function RotatedExtrude (wire, height, rotation, keepWire) {
	if (!wire || wire.IsNull()) { console.error('RotatedExtrude received Null Wire!'); }
	const curExtrusion = CacheOp(arguments, () => {
		const upperPolygon = Rotate([0, 0, 1], rotation, Translate([0, 0, height], wire, true));
		sceneShapes = Remove(sceneShapes, upperPolygon);

		// Define the straight spine going up the middle of the sweep
		const spineWire = BSpline([
			[0, 0, 0],
			[0, 0, height]], false);
		sceneShapes = Remove(sceneShapes, spineWire); // Don't render these

		// Define the guiding helical auxiliary spine (which controls the rotation)
		const steps = 30;
		const aspinePoints = [];
		for (let i = 0; i <= steps; i++) {
			const alpha = i / steps;
			aspinePoints.push([
				20 * Math.sin(alpha * rotation * 0.0174533),
				20 * Math.cos(alpha * rotation * 0.0174533),
				height * alpha]);
		}

		const aspineWire = BSpline(aspinePoints, false);
		sceneShapes = Remove(sceneShapes, aspineWire); // Don't render these

		// Sweep the face wires along the spine to create the extrusion
		const pipe = new oc.BRepOffsetAPI_MakePipeShell(spineWire);
		pipe.SetMode(aspineWire, true);
		pipe.Add(wire);
		pipe.Add(upperPolygon);
		pipe.Build();
		pipe.MakeSolid();
		return new oc.TopoDS_Shape(pipe.Shape());
	});
	if (!keepWire) { sceneShapes = Remove(sceneShapes, wire); }
	sceneShapes.push(curExtrusion);
	return curExtrusion;
}

function Loft (wires, keepWires) {
	const curLoft = CacheOp(arguments, () => {
		const pipe = new oc.BRepOffsetAPI_ThruSections(true);

		// Construct a Loft that passes through the wires
		wires.forEach((wire) => { pipe.AddWire(wire); });

		pipe.Build();
		return new oc.TopoDS_Shape(pipe.Shape());
	});

	wires.forEach((wire) => {
		if (!keepWires) { sceneShapes = Remove(sceneShapes, wire); }
	});
	sceneShapes.push(curLoft);
	return curLoft;
}

function Pipe (shape, wirePath, keepInputs) {
	const curPipe = CacheOp(arguments, () => {
		const pipe = new oc.BRepOffsetAPI_MakePipe(wirePath, shape);
		pipe.Build();
		return new oc.TopoDS_Shape(pipe.Shape());
	});

	if (!keepInputs) {
		sceneShapes = Remove(sceneShapes, shape);
		sceneShapes = Remove(sceneShapes, wirePath);
	}
	sceneShapes.push(curPipe);
	return curPipe;
}

// This is a utility class for drawing wires/shapes with lines, arcs, and splines
// This is unique, it needs to be called with the "new" keyword prepended
function Sketch (startingPoint) {
	this.currentIndex = 0;
	this.faces = [];
	this.wires = [];
	this.firstPoint = new oc.gp_Pnt(startingPoint[0], startingPoint[1], 0);
	this.lastPoint = this.firstPoint;
	this.wireBuilder = new oc.BRepBuilderAPI_MakeWire();
	this.fillets = [];
	this.argsString = ComputeHash(arguments, true);

	// Functions are: BSplineTo, Fillet, Wire, and Face
	this.Start = function (startingPoint) {
		this.firstPoint = new oc.gp_Pnt(startingPoint[0], startingPoint[1], 0);
		this.lastPoint = this.firstPoint;
		this.wireBuilder = new oc.BRepBuilderAPI_MakeWire();
		this.argsString += ComputeHash(arguments, true);
		return this;
	};

	this.End = function (closed, reversed) {
		this.argsString += ComputeHash(arguments, true);

		if (closed &&
			(this.firstPoint.X() !== this.lastPoint.X() ||
				this.firstPoint.Y() !== this.lastPoint.Y())) {
			this.LineTo(this.firstPoint);
		}

		let wire = this.wireBuilder.Wire();
		if (reversed) { wire = wire.Reversed(); }
		wire.hash = stringToHash(this.argsString);
		this.wires.push(wire);

		let faceBuilder = null;
		if (this.faces.length > 0) {
			faceBuilder = new oc.BRepBuilderAPI_MakeFace(this.wires[0]);
			for (let w = 1; w < this.wires.length; w++) {
				faceBuilder.Add(this.wires[w]);
			}
		} else {
			faceBuilder = new oc.BRepBuilderAPI_MakeFace(wire);
		}

		const face = faceBuilder.Face();
		face.hash = stringToHash(this.argsString);
		this.faces.push(face);
		return this;
	};

	this.Wire = function (reversed) {
		this.argsString += ComputeHash(arguments, true);
		// let wire = this.wires[this.wires.length - 1];
		this.applyFillets();
		this.faces[this.faces.length - 1].hash = stringToHash(this.argsString);
		let wire = GetWire(this.faces[this.faces.length - 1]);
		if (reversed) { wire = wire.Reversed(); }
		sceneShapes.push(wire);
		return wire;
	};
	this.Face = function (reversed) {
		this.argsString += ComputeHash(arguments, true);
		this.applyFillets();
		let face = this.faces[this.faces.length - 1];
		if (reversed) { face = face.Reversed(); }
		face.hash = stringToHash(this.argsString);
		sceneShapes.push(face);
		return face;
	};

	this.applyFillets = function () {
		// Add Fillets if Necessary
		if (this.fillets.length > 0) {
			let successes = 0;
			const swapFillets = [];
			for (let f = 0; f < this.fillets.length; f++) { this.fillets[f].disabled = false; }

			// Create Fillet Maker 2D
			const makeFillet = new oc.BRepFilletAPI_MakeFillet2d(this.faces[this.faces.length - 1]);
			// TopExp over the vertices
			ForEachVertex(this.faces[this.faces.length - 1], (vertex) => {
				// Check if the X and Y coords of any vertices match our chosen fillet vertex
				const pnt = oc.BRep_Tool.prototype.Pnt(vertex);
				for (let f = 0; f < this.fillets.length; f++) {
					if (!this.fillets[f].disabled &&
						pnt.X() === this.fillets[f].x &&
						pnt.Y() === this.fillets[f].y) {
						// If so: Add a Radius there!
						makeFillet.AddFillet(vertex, this.fillets[f].radius);
						this.fillets[f].disabled = true;
						successes++;
						break;
					}
				}
			});
			if (successes > 0) { this.faces[this.faces.length - 1] = makeFillet.Shape(); } else { console.log('Couldn\'t find any of the vertices to fillet!!'); }
			this.fillets.concat(swapFillets);
		}
	};

	this.AddWire = function (wire) {
		this.argsString += ComputeHash(arguments, true);
		// This adds another wire (or edge??) to the currently constructing shape...
		this.wireBuilder.Add(wire);
		if (endPoint) { this.lastPoint = endPoint; } // Yike what to do here...?
		return this;
	};

	this.LineTo = function (nextPoint) {
		this.argsString += ComputeHash(arguments, true);
		let endPoint = null;
		if (nextPoint.X) {
			if (this.lastPoint.X() === nextPoint.X() &&
				this.lastPoint.Y() === nextPoint.Y()) { return this; }
			endPoint = nextPoint;
		} else {
			if (this.lastPoint.X() === nextPoint[0] &&
				this.lastPoint.Y() === nextPoint[1]) { return this; }
			endPoint = new oc.gp_Pnt(nextPoint[0], nextPoint[1], 0);
		}
		const lineSegment = new oc.GC_MakeSegment(this.lastPoint, endPoint).Value();
		const lineEdge = new oc.BRepBuilderAPI_MakeEdge(lineSegment).Edge();
		this.wireBuilder.Add(new oc.BRepBuilderAPI_MakeWire(lineEdge).Wire());
		this.lastPoint = endPoint;
		this.currentIndex++;
		return this;
	};

	this.ArcTo = function (pointOnArc, arcEnd) {
		this.argsString += ComputeHash(arguments, true);
		const onArc = new oc.gp_Pnt(pointOnArc[0], pointOnArc[1], 0);
		const nextPoint = new oc.gp_Pnt(arcEnd[0], arcEnd[1], 0);
		const arcCurve = new oc.GC_MakeArcOfCircle(this.lastPoint, onArc, nextPoint).Value();
		const arcEdge = new oc.BRepBuilderAPI_MakeEdge(arcCurve).Edge();
		this.wireBuilder.Add(new oc.BRepBuilderAPI_MakeWire(arcEdge).Wire());
		this.lastPoint = nextPoint;
		this.currentIndex++;
		return this;
	};

	// Constructs an order-N Bezier Curve where the first N-1 points are control points
	// and the last point is the endpoint of the curve
	this.BezierTo = function (bezierControlPoints) {
		this.argsString += ComputeHash(arguments, true);
		const ptList = new oc.TColgp_Array1OfPnt(1, bezierControlPoints.length + 1);
		ptList.SetValue(1, this.lastPoint);
		for (let bInd = 0; bInd < bezierControlPoints.length; bInd++) {
			const ctrlPoint = convertToPnt(bezierControlPoints[bInd]);
			ptList.SetValue(bInd + 2, ctrlPoint);
			this.lastPoint = ctrlPoint;
		}
		const cubicCurve = new oc.Geom_BezierCurve(ptList);
		const handle = new oc.Handle_Geom_BezierCurve(cubicCurve);
		const lineEdge = new oc.BRepBuilderAPI_MakeEdge(handle).Edge();
		this.wireBuilder.Add(new oc.BRepBuilderAPI_MakeWire(lineEdge).Wire());
		this.currentIndex++;
		return this;
	};

	/* Constructs a BSpline from the previous point through this set of points */
	this.BSplineTo = function (bsplinePoints) {
		this.argsString += ComputeHash(arguments, true);
		const ptList = new oc.TColgp_Array1OfPnt(1, bsplinePoints.length + 1);
		ptList.SetValue(1, this.lastPoint);
		for (let bInd = 0; bInd < bsplinePoints.length; bInd++) {
			const ctrlPoint = convertToPnt(bsplinePoints[bInd]);
			ptList.SetValue(bInd + 2, ctrlPoint);
			this.lastPoint = ctrlPoint;
		}
		const handle = new oc.GeomAPI_PointsToBSpline(ptList).Curve();
		const lineEdge = new oc.BRepBuilderAPI_MakeEdge(handle).Edge();
		this.wireBuilder.Add(new oc.BRepBuilderAPI_MakeWire(lineEdge).Wire());
		this.currentIndex++;
		return this;
	};

	this.Fillet = function (radius) {
		this.argsString += ComputeHash(arguments, true);
		this.fillets.push({ x: this.lastPoint.X(), y: this.lastPoint.Y(), radius: radius });
		return this;
	};

	this.Circle = function (center, radius, reversed) {
		this.argsString += ComputeHash(arguments, true);
		const circle = new oc.GC_MakeCircle(new oc.gp_Ax2(convertToPnt(center),
			new oc.gp_Dir(0, 0, 1)), radius).Value();
		const edge = new oc.BRepBuilderAPI_MakeEdge(circle).Edge();
		let wire = new oc.BRepBuilderAPI_MakeWire(edge).Wire();
		if (reversed) { wire = wire.Reversed(); }
		wire.hash = stringToHash(this.argsString);
		this.wires.push(wire);

		let faceBuilder = null;
		if (this.faces.length > 0) {
			faceBuilder = new oc.BRepBuilderAPI_MakeFace(this.wires[0]);
			for (let w = 1; w < this.wires.length; w++) {
				faceBuilder.Add(this.wires[w]);
			}
		} else {
			faceBuilder = new oc.BRepBuilderAPI_MakeFace(wire);
		}
		const face = faceBuilder.Face();
		face.hash = stringToHash(this.argsString);
		this.faces.push(face);
		return this;
	};
}

function SaveFile (filename, fileURL) {
	postMessage({
		'type': 'saveFile',
		'payload': { filename: filename, fileURL: fileURL },
	});
}

function Slider (name = 'Val', defaultValue = 0.5, min = 0.0, max = 1.0, realTime = false, step, precision) {
	if (!(name in GUIState)) { GUIState[name] = defaultValue; }
	if (!step) { step = 0.01; }
	if (typeof precision === 'undefined') {
		precision = 2;
	} else if (precision % 1) { console.error('Slider precision must be an integer'); }

	postMessage({ 'type': 'addSlider', 'payload': { name: name, default: defaultValue, min: min, max: max, realTime: realTime, step: step, dp: precision } });
	return GUIState[name];
}

function Button (name = 'Action') {
	postMessage({ 'type': 'addButton', 'payload': { name: name } });
}

function Checkbox (name = 'Toggle', defaultValue = false) {
	if (!(name in GUIState)) { GUIState[name] = defaultValue; }
	postMessage({ 'type': 'addCheckbox', 'payload': { name: name, default: defaultValue } });
	return GUIState[name];
}

function TextInput (name = 'Text', defaultValue = '', realTime = false) {
	if (!(name in GUIState)) { GUIState[name] = defaultValue; }
	postMessage({ 'type': 'addTextbox', 'payload': { name: name, default: defaultValue, realTime: realTime } });
	return GUIState[name];
}

function Dropdown (name = 'Dropdown', defaultValue = '', options = {}, realTime = false) {
	if (!(name in GUIState)) { GUIState[name] = defaultValue; }
	postMessage({ 'type': 'addDropdown', 'payload': { name: name, default: defaultValue, options: options, realTime: realTime } });
	return GUIState[name];
}
